package club.lemos.websocketdemo.conf;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //参考 https://stackoverflow.com/questions/37853727/where-user-comes-from-in-convertandsendtouser-works-in-sockjsspring-websocket
        //点对点单聊的实现方案：
        //默认情况下，建立连接的握手阶段确定用户的方法由 AbstractHandshakeHandler#determineUser 提供，返回值是一个 java.security.Principal
        //注：实际应用中，亦可直接重写 AbstractHandshakeHandler#determineUser，可以有如下方案：
        // ① 通过 addEndpoint("/ws/{id}") ，每个客户端连接时可以有自己的路径。那么，连接阶段 determineUser 即可从 uri 中提取当前连接的id 作为用户id
        // ② 通过 cookie/session 获取建立连接的用户的信息。
        //转发消息时调用 convertAndSendToUser(<Principal.name>, ...) 方法，底层是 convertAndSend(/user/<Principal.name>/...
        //消息处理阶段，若发现消息是 /user 开头，
        // 首先会提取 /user/<Principal.name>/... 中的 Principal.name；
        // 其次在 session 列表中遍历所有连接的握手阶段的用户是 Principal.name 的那个连接，
        // 最后，将消息转发给这个连接的客户端，并将路由改成 /user/...。
        // （参考
        // SimpMessagingTemplate.setUserDestinationPrefix
        // DefaultUserDestinationResolver.setUserDestinationPrefix、
        // AbstractMessageBrokerConfiguration
        // MessageBrokerRegistry#setUserDestinationPrefix 请参看此方法的注释
        // ）
        //客户端，只需要订阅 /user/... 即可接收此消息
        registry.addEndpoint("/ws").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        //路由到消息处理方法 message-handling methods
        registry.setApplicationDestinationPrefixes("/app");
        //路由到代理服务 message broker
        //simple in-memory message broker
        registry.enableSimpleBroker("/topic");
    }

    //消息存储的实现方案：

    //离线消息与历史消息（消息漫游）区别？
    //离线消息是指接收者不在线，收到的消息；历史消息包括所有发送与接收到的消息。离线消息通常存储在redis缓存中，采用 SortedSet，且只保存7天。
    // 而历史消息通常存储在关系型数据库中。
    //应对场景不同：
    // 历史消息：当设备需要读取未缓存在设备上的历史消息时，即为消息漫游，或者对于新设备想同步获取到另一台设备上的历史消息。
    // 离线消息：每个用户打开App就需要拉取离线；网络中断重连后需要拉取离线；收到消息序列号不连续需要拉取离线消息。

    //历史消息，服务端将每个会话的完整消息按会话ID去保存。
    // 采用这样的存储方式，由于同一个会话的消息会被该会话中的所有用户去拉，因此是读扩散。
    // 此外，历史消息通常在读取离线消息和设备上保存的消息后，获取更多之前的消息时调用。
    //离线消息，服务端将会话中每个离线用户的离线消息按先后顺序分别保存到各自的收件箱。
    // 采用这样的存储方式，同一个会话中的每条离线消息都需要向该会话中的所有离线用户的收件箱中去保存，因此是写扩散。
    // 离线消息通常保存在缓存中，每个离线用户在下次上线后，只需要从收件箱中将所有离线消息全部读出来即可，完了将离线消息从服务端缓存中删除。
    // 离线消息的处理优化：每个会话的最近14天的消息只做一次缓存。会话中的消息是有先后顺序的，每个用户的收件箱只需要保存最后一次收到的消息ID（last_ack_msg_id）,
    // 然后，从这个消息ID开始拉取所有未收到的消息，最后更新last_ack_msg_id。

    //可以仅采用离线消息方案，由于只使用缓存，且大部分消息都缓存在客户端，因此减轻了服务器的存储压力。缺点是更换新设备后没法同步之前设备的消息。
    //可以仅采用历史消息方案，客户端不需要做缓存，亦不需要保存last_ack_msg_id。
    // 缺点是客户端每次启动都要从服务端拉取之前的历史消息（因为设备没有缓存历史消息），且历史消息保存在关系型数据库的时间范围较长。

    //如何保存 msg_id 有序地生成？
    // redis + 数据库保存每个会话的last_msg_id。如果redis为空，则从数据库中取msg_id，如果数据库为空，则msg_id为0。
}
